/*
 * Copyright 2007-2008 Andrew O'Malley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package ws.quokka.plugin.resources;

import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.FilterSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.PatternSet;

import ws.quokka.core.bootstrap_util.IOUtils;
import ws.quokka.core.bootstrap_util.PropertiesUtil;
import ws.quokka.core.plugin_spi.support.TypedProperties;
import ws.quokka.core.repo_spi.RepoArtifact;

import ws.quokka.plugin.lifecycle.LifeCycleImpl;

import java.io.File;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;


/**
 *
 */
public class ResourcesPlugin extends LifeCycleImpl {
    //~ Static fields/initializers -------------------------------------------------------------------------------------

    private static final String OUTPUT = "output";
    private static final String INCLUDE_GENERATED_RESOURCES = "includeGeneratedResources";
    private static final String FILTERED = "filtered";
    private static final String FILTERS = "filters";
    private static final String UNFILTERED = "unfiltered";

    //~ Instance fields ------------------------------------------------------------------------------------------------

    private IOUtils utils = new IOUtils();

    //~ Methods --------------------------------------------------------------------------------------------------------

    public void extract() {
        List path = getResources().getPathGroup("artifactPath");

        assertTrue(path.size() == 1,
            getResources().getPrefix()
            + ".artifactPath must refer to the id of a project path containing a single artifact");

        RepoArtifact artifact = (RepoArtifact)path.get(0);

        // Root dir for resources
        String root = getProperties().getString("resources.set.dir");

        if (!root.startsWith("/")) {
            root = "/META-INF/quokka/" + artifact.getId().toPathString() + "/" + root;
        }

        // Expand the .jar file
        File dir = artifact.getLocalCopy();

        if (dir.isFile()) {
            Expand expand = (Expand)getUtils().init(new Expand(), "unjar");
            expand.setSrc(artifact.getLocalCopy());

            PatternSet patternSet = new PatternSet();
            PatternSet.NameEntry entry = patternSet.createInclude();
            entry.setName(root + "/**");
            expand.addPatternset(patternSet);

            // TODO: Eliminate expanding to a temporary dir
            // The code below removes the root properly, however includes and excludes need fixing as well
            //            final String root1 = root;
            //            expand.add(new FileNameMapper() {
            //                public void setFrom(String from) {
            //                }
            //
            //                public void setTo(String to) {
            //                }
            //
            //                public String[] mapFileName(String sourceFileName) {
            //                    return new String[] {sourceFileName.substring(root1.length())};
            //                }
            //            });
            dir = getUtils().normalise(getTargetDir().getAbsolutePath() + "/temp/extract/" + getResources().getPrefix());
            expand.setDest(dir);
            expand.perform();
        }

        // Override the fileset directory to make it relative to the expanded artifact
        // If dir specified has a leading /, use the supplied value otherwise prefix with the artifact resources dir
        FileSet resources = getProperties().getFileSet("resources");
        resources.setDir(getUtils().normalise(dir.getAbsolutePath() + root));

        Copy copy = (Copy)getUtils().init(new Copy(), "copy");
        copy.setTodir(getProperties().getFile("toDir"));
        copy.addFileset(resources);
        copy.perform();
    }

    public void process() {
        processResources(getLifeCycle().getState().getFilterableResources(),
            getLifeCycle().getState().getNonFilterableResources(), getProperties().getFile(OUTPUT));
    }

    public void processTest() {
        processResources(getLifeCycle().getState().getNonFilterableTestResources(),
            getLifeCycle().getState().getNonFilterableTestResources(), getProperties().getFile(OUTPUT));
    }

    private void processResources(List filtereDResources, List unfilteredResources, File output) {
        boolean includeGenerated = getProperties().getBoolean(INCLUDE_GENERATED_RESOURCES, false);

        // Copy filtered resources
        logger().verbose("Copying filtered resources");
        filtereDResources = new ArrayList(includeGenerated ? filtereDResources : Collections.EMPTY_LIST);
        filtereDResources.addAll(getProperties().getMap(FILTERED, false, FileSet.class).values());
        copyResources(filtereDResources, output, getProperties().getMap(FILTERS, false, FileSet.class));

        // Copy unfiltered resources
        logger().verbose("Copying unfiltered resources");
        unfilteredResources = new ArrayList(includeGenerated ? unfilteredResources : Collections.EMPTY_LIST);
        unfilteredResources.addAll(getProperties().getMap(UNFILTERED, false, FileSet.class).values());
        copyResources(unfilteredResources, output, null);
    }

    private void copyResources(List resources, File destination, Map filtersMap) {
        // Stores tokens to detect change from last run
        File tokensFile = getUtils().normalise(getTargetDir().getAbsolutePath() + "/temp/"
                + getResources().getTargetName().substring(getResources().getTargetName().indexOf(":") + 1) + "/"
                + ((filtersMap == null) ? FILTERED : UNFILTERED) + ".properties");

        resources = (List)getUtils().filterExisting(resources);

        if (resources.size() == 0) {
            return; // No resources to copy
        }

        Copy copy = (Copy)getUtils().createTask("copy");
        copy.setTodir(destination);
        copy.setFiltering(false);

        for (Iterator i = resources.iterator(); i.hasNext();) {
            FileSet fileSet = (FileSet)i.next();
            copy.addFileset(fileSet);
        }

        if (filtersMap != null) {
            // Apply filtering
            copy.setFiltering(true);

            FilterSet filterSet = copy.createFilterSet();

            Properties tokens = getTokens(filtersMap);
            boolean match = match(tokens, tokensFile);

            if (!match) {
                logger().verbose("Forcing resources overwrite as tokens have changed");
                copy.setOverwrite(true);
                getUtils().mkdir(tokensFile.getParentFile());
                utils.saveProperties(tokensFile, tokens);
            }

            for (Iterator i = tokens.entrySet().iterator(); i.hasNext();) {
                Map.Entry entry = (Map.Entry)i.next();
                String key = (String)entry.getKey();
                String value = (String)entry.getValue();
                filterSet.addFilter(key, value);
                filterSet.addFilter(key + "--norm", normalisePath(value));
                filterSet.addFilter(key + "--esc", escape(value));
                filterSet.addFilter(key + "--norm,esc", escape(normalisePath(value)));
            }
        }

        copy.perform();
    }

    private boolean match(Properties tokens, File tokensFile) {
        if (!tokensFile.exists()) {
            return false;
        }

        String prefix = getProperties().getString("ignoredTokensPrefix", "quokka.resources.");
        TypedProperties ph = new TypedProperties(prefix);
        ph.setProperties(PropertiesUtil.getProperties(getProject()));
        ph.setProject(getProject());

        Map ignored = ph.getMap("ignoredTokens", false, String.class);

        // Force match if any tokens have changed
        boolean match = true;
        Properties oldTokens = utils.loadProperties(tokensFile);

        for (Iterator i = tokens.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            String key = (String)entry.getKey();

            if (ignored.containsKey(key)) {
                continue;
            }

            String value = (String)entry.getValue();
            String oldValue = (String)oldTokens.remove(key);

            if (oldValue == null) {
                match = false;
                logger().verbose("New token: key=" + key + ", value=" + value);
            } else {
                if (!value.equals(oldValue)) {
                    match = false;
                    logger().verbose("Token changed: key=" + key + "\n\told=" + oldValue + "\n\tnew=" + value);
                }
            }
        }

        for (Iterator i = oldTokens.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            String key = (String)entry.getKey();

            if (ignored.containsKey(key)) {
                continue;
            }

            match = false;
            logger().verbose("Token removed: key=" + key + ", value=" + entry.getValue());
        }

        return match;
    }

    private Properties getTokens(Map filtersMap) {
        Properties tokens = new Properties();
        boolean addProperties = getProperties().getBoolean("addPropertiesAsTokens", true);

        if (addProperties) {
            tokens.putAll(PropertiesUtil.getProperties(getProject()));
        }

        for (Iterator i = filtersMap.values().iterator(); i.hasNext();) {
            FileSet filters = (FileSet)i.next();

            if (!filters.getDir().exists()) {
                continue;
            }

            tokens.putAll(getTokens(filters));
        }

        return tokens;
    }

    private String escape(String value) {
        StringBuffer sb = new StringBuffer();

        for (int i = 0; i < value.length(); i++) {
            char ch = value.charAt(i);

            if (ch == '\\') {
                sb.append('\\');
            }

            sb.append(ch);
        }

        return sb.toString();
    }

    private String normalisePath(String value) {
        try {
            Path path = new Path(getProject(), value);

            return path.toString();
        } catch (Exception e) {
            e.printStackTrace();

            return value; // Not a valid path
        }
    }

    private Map getTokens(FileSet filters) {
        Properties tokens = new Properties();

        for (Iterator i = getUtils().getFiles(filters).iterator(); i.hasNext();) {
            File file = (File)i.next();
            tokens.putAll(utils.loadProperties(file));
        }

        return tokens;
    }
}
